Key

scroll

Key Blog

  • Key 主頁>
  • 博客>
  • [ue5] 根據地面的傾斜調整角色傾斜的方法與解說
  • [UE5] 根據地面的傾斜調整角色傾斜的方法與解說

    @kiikey4(Key Zhao)

    [UE5] 根據地面的傾斜調整角色傾斜的方法與解說

    最後更新日期 2024年10月20日

    發佈日期 2024年10月17日

    0

    概要

    本文將介紹如何用C++實現將角色根據地面傾斜度傾斜的處理。

    如果你想用Blueprint來實現,請參考這篇文章。 UE4 根據地面傾斜度傾斜角色

    環境

    • Rider 2024.2.6
    • Unreal Engine 5.4

    參考資料

    本篇

    當角色走在斜坡上時,如果不調整傾斜度會變成這樣。 MouseRunOnBoardWithoutAlignFloor_cn0zje 角色的頭部陷入斜坡,看起來不太自然。

    那麼,讓我們開始用C++來解決吧。

    步驟如下

    1. 對著角色正下方進行射線檢測(Raycast)
    2. 如果射線檢測撞到地面(斜坡),則獲取地面(斜坡)的法線
    3. 利用獲得的法線來計算地面(斜坡)的傾斜度
    4. 根據地面(斜坡)的傾斜度來旋轉角色

    在玩家Class中編寫AlignFloor()函數

    AlignFloor()每0.1秒透過計時器呼叫一次(也可以在Tick中呼叫,但考慮到優化設置為0.1秒,從外觀上看,0.1秒的頻率應該不會造成明顯的違和感)。

    PlayerCharacter.h
    1private: 2 void AlignFloor() const; 3 4 FTimerHandle AlignFloorTimerHandle;
    PlayerCharacter.cpp
    1 2void APlayerCharacter::BeginPlay() 3{ 4 Super::BeginPlay(); 5 6 GetWorldTimerManager().SetTimer(AlignFloorTimerHandle, this, &APlayerCharacter::AlignFloor, 0.1f, true); 7} 8 9void APlayerCharacter::AlignFloor() const 10{ 11 const FVector MeshLocation = GetMesh()->GetComponentLocation() + 1.f * FVector::UpVector; 12 const FVector MeshDownLocation = MeshLocation - 1000.f * FVector::UpVector; 13 FHitResult HitResult; 14 FCollisionQueryParams CollisionQueryParams; 15 CollisionQueryParams.AddIgnoredActor(this); 16 const bool IsHit = GetWorld()->LineTraceSingleByChannel(HitResult, MeshLocation, MeshDownLocation, ECC_WorldStatic, 17 CollisionQueryParams); 18 if (IsHit) 19 { 20 FVector FloorNormal = HitResult.ImpactNormal; 21 FVector RightVector = GetActorRightVector(); 22 FVector UpVector = GetActorUpVector(); 23 float SlopePitch; 24 float SlopeRoll; 25 UKismetMathLibrary::GetSlopeDegreeAngles(RightVector, FloorNormal, UpVector, SlopePitch, SlopeRoll); 26 SlopePitch = -SlopePitch; 27 const float MeshYaw = GetMesh()->GetComponentRotation().Yaw; 28 const float MeshPitch = GetMesh()->GetComponentRotation().Pitch; 29 const FRotator FloorRotation = FRotator(MeshPitch, MeshYaw, SlopePitch); 30 GetMesh()->SetWorldRotation(FloorRotation); 31 } 32}

    這樣就完成了根據地面傾斜度傾斜角色的實現!

    解釋

    對著角色正下方進行射線檢測

    PlayerCharacter.cpp
    1void APlayerCharacter::AlignFloor() const 2{ 3 const FVector MeshLocation = GetMesh()->GetComponentLocation() + 1.f * FVector::UpVector; 4 const FVector MeshDownLocation = MeshLocation - 1000.f * FVector::UpVector; 5 FHitResult HitResult; 6 FCollisionQueryParams CollisionQueryParams; 7 CollisionQueryParams.AddIgnoredActor(this); 8 const bool IsHit = GetWorld()->LineTraceSingleByChannel(HitResult, MeshLocation, MeshDownLocation, ECC_WorldStatic, 9 CollisionQueryParams); 10 if (IsHit) 11 { 12 FVector FloorNormal = HitResult.ImpactNormal; 13 FVector RightVector = GetActorRightVector(); 14 FVector UpVector = GetActorUpVector(); 15 float SlopePitch; 16 float SlopeRoll; 17 UKismetMathLibrary::GetSlopeDegreeAngles(RightVector, FloorNormal, UpVector, SlopePitch, SlopeRoll); 18 SlopePitch = -SlopePitch; 19 const float MeshYaw = GetMesh()->GetComponentRotation().Yaw; 20 const float MeshPitch = GetMesh()->GetComponentRotation().Pitch; 21 const FRotator FloorRotation = FRotator(MeshPitch, MeshYaw, SlopePitch); 22 GetMesh()->SetWorldRotation(FloorRotation); 23 } 24}

    這部分是從角色的稍微上方來向正下方進行射線檢測。

    const FVector MeshLocation = GetMesh()->GetComponentLocation() + 1.f * FVector::UpVector;

    + 1.f 是為了確保與地面之間的距離。如果不這樣做,角色可能會和地面位於同一高度,導致射線檢測無法正確接觸到地面(這在我的環境中發生過)。

    如果射線檢測擊中地面,則獲取地面的法線(FloorNormal)

    利用獲取的法線來計算地面的傾斜度

    PlayerCharacter.cpp
    1void APlayerCharacter::AlignFloor() const 2{ 3 const FVector MeshLocation = GetMesh()->GetComponentLocation() + 1.f * FVector::UpVector; 4 const FVector MeshDownLocation = MeshLocation - 1000.f * FVector::UpVector; 5 FHitResult HitResult; 6 FCollisionQueryParams CollisionQueryParams; 7 CollisionQueryParams.AddIgnoredActor(this); 8 const bool IsHit = GetWorld()->LineTraceSingleByChannel(HitResult, MeshLocation, MeshDownLocation, ECC_WorldStatic, 9 CollisionQueryParams); 10 if (IsHit) 11 { 12 FVector FloorNormal = HitResult.ImpactNormal; 13 FVector RightVector = GetActorRightVector(); 14 FVector UpVector = GetActorUpVector(); 15 float SlopePitch; 16 float SlopeRoll; 17 UKismetMathLibrary::GetSlopeDegreeAngles(RightVector, FloorNormal, UpVector, SlopePitch, SlopeRoll); 18 SlopePitch = -SlopePitch; 19 const float MeshYaw = GetMesh()->GetComponentRotation().Yaw; 20 const float MeshPitch = GetMesh()->GetComponentRotation().Pitch; 21 const FRotator FloorRotation = FRotator(MeshPitch, MeshYaw, SlopePitch); 22 GetMesh()->SetWorldRotation(FloorRotation); 23 } 24}

    這部分的數學細節稍微複雜,所以有興趣的話可以繼續閱讀,對數學不太感興趣的可以跳過。

    假設角色是一隻老鼠,想像一下這隻老鼠站在斜坡上的情況。當射線檢測擊中斜坡時,我們會獲取到它的法線。

    獲取地面(斜坡)的法線。

    FVector FloorNormal = HitResult.ImpactNormal;

    法線是什麼?

    在曲面上一個點的法線,是與該點的切平面垂直的直線。

    計算傾斜角度

    獲取角色的右方向向量和上方向向量。

    1 //... 2 FVector RightVector = GetActorRightVector(); 3 FVector UpVector = GetActorUpVector(); 4 //...

    我們使用 UKismetMathLibrary 的函數來獲取斜坡的傾斜角度(SlopePitch)。 UKismetMathLibrary::GetSlopeDegreeAngles(RightVector, FloorNormal, UpVector, SlopePitch, SlopeRoll);

    如果查看這個函數的內部實現,會看到以下的計算過程。

    KismetMathLibary.cpp
    1void UKismetMathLibrary::GetSlopeDegreeAngles(const FVector& MyRightYAxis, const FVector& FloorNormal, const FVector& UpVector, float& OutSlopePitchDegreeAngle, float& OutSlopeRollDegreeAngle) 2{ 3 const FVector FloorZAxis = FloorNormal; 4 const FVector FloorXAxis = MyRightYAxis ^ FloorZAxis; 5 const FVector FloorYAxis = FloorZAxis ^ FloorXAxis; 6 7 OutSlopePitchDegreeAngle = 90.f - FMath::RadiansToDegrees(FMath::Acos(FloorXAxis | UpVector)); 8 OutSlopeRollDegreeAngle = 90.f - FMath::RadiansToDegrees(FMath::Acos(FloorYAxis | UpVector)); 9}

    用圖來解釋可能會更容易理解。

    例如,當老鼠站在斜坡上時,右側面圖如下所示:

    CalculateSlope_f2t9dk

    △三角形是老鼠。

    FloorZ 是斜坡的法線(法線向量)。FloorX老鼠的右方向向量FloorZ(法線向量)的叉積(Cross Product),結果是斜坡的上升方向向量。

    在 FVector 中,插入符號(Caret)「^」是叉積的運算符。

    兩個向量的叉積結果是垂直於這兩個向量的向量。

    叉積詳情:https://ja.wikipedia.org/wiki/%E3%82%AF%E3%83%AD%E3%82%B9%E7%A9%8D

    接下來,計算 FloorZFloorX 的叉積,可以得到「老鼠的右方向向量」。

    const FVector FloorYAxis = FloorZAxis ^ FloorXAxis;

    然後計算斜坡的上升方向向量(FloorX)和老鼠的垂直向上方向向量(Up)的點積(Dot Product),再通過對結果取 Arccos,得到角度 a。從 90 度中減去這個角度,就能獲得斜坡的傾斜角度(SlopePitch)。

    OutSlopePitchDegreeAngle = 90.f - FMath::RadiansToDegrees(FMath::Acos(FloorXAxis | UpVector));

    在 FVector 中,「|」是點積的運算符。

    點積詳情:https://ja.wikipedia.org/wiki/%E3%83%89%E3%83%83%E3%83%88%E7%A9%8D 點積的公式是:若向量 u 和 v 之間的角度為 θ,則有以下公式: u⋅v=∣u∣∣v∣cosθ

    根據地面傾斜的量來旋轉角色
    PlayerCharacter.cpp
    1void APlayerCharacter::AlignFloor() const 2{ 3 const FVector MeshLocation = GetMesh()->GetComponentLocation() + 1.f * FVector::UpVector; 4 const FVector MeshDownLocation = MeshLocation - 1000.f * FVector::UpVector; 5 FHitResult HitResult; 6 FCollisionQueryParams CollisionQueryParams; 7 CollisionQueryParams.AddIgnoredActor(this); 8 const bool IsHit = GetWorld()->LineTraceSingleByChannel(HitResult, MeshLocation, MeshDownLocation, ECC_WorldStatic, 9 CollisionQueryParams); 10 if (IsHit) 11 { 12 FVector FloorNormal = HitResult.ImpactNormal; 13 FVector RightVector = GetActorRightVector(); 14 FVector UpVector = GetActorUpVector(); 15 float SlopePitch; 16 float SlopeRoll; 17 UKismetMathLibrary::GetSlopeDegreeAngles(RightVector, FloorNormal, UpVector, SlopePitch, SlopeRoll); 18 SlopePitch = -SlopePitch; 19 const float MeshYaw = GetMesh()->GetComponentRotation().Yaw; 20 const float MeshPitch = GetMesh()->GetComponentRotation().Pitch; 21 const FRotator FloorRotation = FRotator(MeshPitch, MeshYaw, SlopePitch); 22 GetMesh()->SetWorldRotation(FloorRotation); 23 } 24}

    通過將老鼠的 Mesh 的滾轉(Roll)反向旋轉,使角色自然地適應斜坡的傾斜。

    滾轉、俯仰、偏航

    結果

    你可以在以下的演示中看到角色正確地適應斜坡並運行的情況。

    最後

    本文介紹了如何根據地面的傾斜來旋轉角色。如果有任何錯誤,請隨時在評論中告訴我們。

    0

    評論

    沒有評論

    發表閣下的感受